package org.hotswap.agent.plugin.spring.scanner; import java.io.IOException; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.hotswap.agent.logging.AgentLogger; import org.hotswap.agent.plugin.spring.ResetBeanPostProcessorCaches; import org.hotswap.agent.plugin.spring.ResetRequestMappingCaches; import org.hotswap.agent.plugin.spring.ResetSpringStaticCaches; import org.hotswap.agent.plugin.spring.SpringPlugin; import org.hotswap.agent.plugin.spring.getbean.ProxyReplacer; import org.hotswap.agent.util.PluginManagerInvoker; import org.hotswap.agent.util.ReflectionHelper; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.context.annotation.ClassPathBeanDefinitionScanner; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.context.annotation.ScannedGenericBeanDefinition; import org.springframework.context.annotation.ScopeMetadata; import org.springframework.context.annotation.ScopeMetadataResolver; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.Resource; import org.springframework.core.type.classreading.CachingMetadataReaderFactory; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; /** * Registers * * @author Jiri Bubnik */ public class ClassPathBeanDefinitionScannerAgent { private static AgentLogger LOGGER = AgentLogger.getLogger(ClassPathBeanDefinitionScannerAgent.class); private static Map<ClassPathBeanDefinitionScanner, ClassPathBeanDefinitionScannerAgent> instances = new HashMap<ClassPathBeanDefinitionScanner, ClassPathBeanDefinitionScannerAgent>(); /** * Flag to check reload status. * In unit test we need to wait for reload finish before the test can continue. Set flag to true * in the test class and wait until the flag is false again. */ public static boolean reloadFlag = false; // target scanner this agent shadows ClassPathBeanDefinitionScanner scanner; // list of basePackages registered with target scanner Set<String> basePackages = new HashSet<String>(); // registry obtained from the scanner BeanDefinitionRegistry registry; // metadata resolver obtained from the scanner ScopeMetadataResolver scopeMetadataResolver; // bean name generator obtained from the scanner BeanNameGenerator beanNameGenerator; /** * Return an agent instance for a scanner. If the instance does not exists yet, it is created. * @param scanner the scanner * @return agent instance */ public static ClassPathBeanDefinitionScannerAgent getInstance(ClassPathBeanDefinitionScanner scanner) { if (!instances.containsKey(scanner)) { instances.put(scanner, new ClassPathBeanDefinitionScannerAgent(scanner)); } return instances.get(scanner); } /** * Find scanner agent by base package. * * @param basePackage the scanner agent or null if no such agent exists * @return the agent */ public static ClassPathBeanDefinitionScannerAgent getInstance(String basePackage) { for (ClassPathBeanDefinitionScannerAgent scannerAgent : instances.values()) { if (scannerAgent.basePackages.contains(basePackage)) return scannerAgent; } return null; } // Create new instance from getInstance(ClassPathBeanDefinitionScanner scanner) and obtain services from the scanner private ClassPathBeanDefinitionScannerAgent(ClassPathBeanDefinitionScanner scanner) { this.scanner = scanner; this.registry = scanner.getRegistry(); this.scopeMetadataResolver = (ScopeMetadataResolver) ReflectionHelper.get(scanner, "scopeMetadataResolver"); this.beanNameGenerator = (BeanNameGenerator) ReflectionHelper.get(scanner, "beanNameGenerator"); } /** * Initialize base package from ClassPathBeanDefinitionScanner.scan() (hooked by a Transformer) * @param basePackage package that Spring will scan */ public void registerBasePackage(String basePackage) { this.basePackages.add(basePackage); PluginManagerInvoker.callPluginMethod(SpringPlugin.class, getClass().getClassLoader(), "registerComponentScanBasePackage", new Class[]{String.class}, new Object[]{basePackage}); } /** * Called by a reflection command from SpringPlugin transformer. * * @param basePackage base package on witch the transformer was registered, used to obtain associated scanner. * @param classDefinition new class definition * @throws IOException error working with classDefinition */ public static void refreshClass(String basePackage, byte[] classDefinition) throws IOException { ClassPathBeanDefinitionScannerAgent scannerAgent = getInstance(basePackage); if (scannerAgent == null) { LOGGER.error("basePackage '{}' not associated with any scannerAgent", basePackage); return; } BeanDefinition beanDefinition = scannerAgent.resolveBeanDefinition(classDefinition); if (beanDefinition != null) { scannerAgent.defineBean(beanDefinition); } reloadFlag = false; } /** * Resolve candidate to a bean definition and (re)load in Spring. * Synchronize to avoid parallel bean definition - usually on reload the beans are interrelated * and parallel load will cause concurrent modification exception. * * @param candidate the candidate to reload */ public void defineBean(BeanDefinition candidate) { synchronized (getClass()) { // TODO sychronize on DefaultListableFactory.beanDefinitionMap? ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate); candidate.setScope(scopeMetadata.getScopeName()); String beanName = this.beanNameGenerator.generateBeanName(candidate, registry); if (candidate instanceof AbstractBeanDefinition) { postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName); } if (candidate instanceof AnnotatedBeanDefinition) { processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate); } removeIfExists(beanName); if (checkCandidate(beanName, candidate)) { BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName); definitionHolder = applyScopedProxyMode(scopeMetadata, definitionHolder, registry); LOGGER.reload("Registering Spring bean '{}'", beanName); LOGGER.debug("Bean definition '{}'", beanName, candidate); registerBeanDefinition(definitionHolder, registry); DefaultListableBeanFactory bf = maybeRegistryToBeanFactory(); if (bf != null) ResetRequestMappingCaches.reset(bf); freezeConfiguration(); } ProxyReplacer.clearAllProxies(); } } /** * If registry contains the bean, remove it first (destroying existing singletons). * @param beanName name of the bean */ private void removeIfExists(String beanName) { if (registry.containsBeanDefinition(beanName)) { LOGGER.debug("Removing bean definition '{}'", beanName); registry.removeBeanDefinition(beanName); ResetSpringStaticCaches.reset(); DefaultListableBeanFactory bf = maybeRegistryToBeanFactory(); if (bf != null) { ResetBeanPostProcessorCaches.reset(bf); } } } private DefaultListableBeanFactory maybeRegistryToBeanFactory() { if (registry instanceof DefaultListableBeanFactory) { return (DefaultListableBeanFactory)registry; } else if (registry instanceof GenericApplicationContext) { return ((GenericApplicationContext) registry).getDefaultListableBeanFactory(); } return null; } // rerun freez configuration - this method is enhanced with cache reset private void freezeConfiguration() { if (registry instanceof DefaultListableBeanFactory) { ((DefaultListableBeanFactory)registry).freezeConfiguration(); } else if (registry instanceof GenericApplicationContext) { (((GenericApplicationContext) registry).getDefaultListableBeanFactory()).freezeConfiguration(); } } /** * Resolve bean definition from class definition if applicable. * * @param bytes class definition. * @return the definition or null if not a spring bean * @throws IOException */ public BeanDefinition resolveBeanDefinition(byte[] bytes) throws IOException { Resource resource = new ByteArrayResource(bytes); resetCachingMetadataReaderFactoryCache(); MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource); if (isCandidateComponent(metadataReader)) { ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader); sbd.setResource(resource); sbd.setSource(resource); if (isCandidateComponent(sbd)) { LOGGER.debug("Identified candidate component class '{}'", metadataReader.getClassMetadata().getClassName()); return sbd; } else { LOGGER.debug("Ignored because not a concrete top-level class '{}'", metadataReader.getClassMetadata().getClassName()); return null; } } else { LOGGER.trace("Ignored because not matching any filter '{}' ", metadataReader.getClassMetadata().getClassName()); return null; } } private MetadataReaderFactory getMetadataReaderFactory() { return (MetadataReaderFactory) ReflectionHelper.get(scanner, "metadataReaderFactory"); } // metadataReader contains cache of loaded classes, reset this cache before BeanDefinition is resolved private void resetCachingMetadataReaderFactoryCache() { if (getMetadataReaderFactory() instanceof CachingMetadataReaderFactory) { Map metadataReaderCache = (Map) ReflectionHelper.getNoException(getMetadataReaderFactory(), CachingMetadataReaderFactory.class, "metadataReaderCache"); if (metadataReaderCache == null) metadataReaderCache = (Map) ReflectionHelper.getNoException(getMetadataReaderFactory(), CachingMetadataReaderFactory.class, "classReaderCache"); if (metadataReaderCache != null) { metadataReaderCache.clear(); LOGGER.debug("Cache cleared: CachingMetadataReaderFactory.clearCache()"); } else { LOGGER.warning("Cache NOT cleared: neither CachingMetadataReaderFactory.metadataReaderCache nor clearCache does not exist."); } } } //////////////////////////////////////////////////////////////////////////////////////////// // Access private / protected members //////////////////////////////////////////////////////////////////////////////////////////// private BeanDefinitionHolder applyScopedProxyMode( ScopeMetadata metadata, BeanDefinitionHolder definition, BeanDefinitionRegistry registry) { return (BeanDefinitionHolder) ReflectionHelper.invoke(null, AnnotationConfigUtils.class, "applyScopedProxyMode", new Class[]{ScopeMetadata.class, BeanDefinitionHolder.class, BeanDefinitionRegistry.class}, metadata, definition, registry); } private void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) { ReflectionHelper.invoke(scanner, ClassPathBeanDefinitionScanner.class, "registerBeanDefinition", new Class[]{BeanDefinitionHolder.class, BeanDefinitionRegistry.class}, definitionHolder, registry); } private boolean checkCandidate(String beanName, BeanDefinition candidate) { return (Boolean) ReflectionHelper.invoke(scanner, ClassPathBeanDefinitionScanner.class, "checkCandidate", new Class[]{String.class, BeanDefinition.class}, beanName, candidate); } private void processCommonDefinitionAnnotations(AnnotatedBeanDefinition candidate) { ReflectionHelper.invoke(null, AnnotationConfigUtils.class, "processCommonDefinitionAnnotations", new Class[]{AnnotatedBeanDefinition.class}, candidate); } private void postProcessBeanDefinition(AbstractBeanDefinition candidate, String beanName) { ReflectionHelper.invoke(scanner, ClassPathBeanDefinitionScanner.class, "postProcessBeanDefinition", new Class[]{AbstractBeanDefinition.class, String.class}, candidate, beanName); } private boolean isCandidateComponent(AnnotatedBeanDefinition sbd) { return (Boolean) ReflectionHelper.invoke(scanner, ClassPathScanningCandidateComponentProvider.class, "isCandidateComponent", new Class[]{AnnotatedBeanDefinition.class}, sbd); } private boolean isCandidateComponent(MetadataReader metadataReader) { return (Boolean) ReflectionHelper.invoke(scanner, ClassPathScanningCandidateComponentProvider.class, "isCandidateComponent", new Class[]{MetadataReader.class}, metadataReader); } }